import os
from copy import deepcopy
from itertools import chain

def checkThree(lis):													# controlla che una lista di tre elementi non vinca (serve dopo)
	if len(set(lis))==1: 												# se la riga i ha tutti gli elementi uguali (i set non possono avere copie)
		if lis[0]=='x': 												# controlla se è x, basta la prima cella, tanto sono uguali
			return True, 1												# restituisce 1, x ha vinto
		if lis[0]=='o': 												# controlla se è o
			return True, 2												# restituisce 2, o ha vinto

	return False, 0														# niente, restituisce False



class grid:																# rappresentazione della griglia


	def __init__(self, pos=[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']], turn=True): # costruttore, accetta anche stati parziali
		self.status=pos
		self.turn=turn



	def print(self):													# stampa la griglia
		print("\n")
		for row in self.status:
			print(f"|{"|".join(row)}|")

		print("")



	def isValid(self, move):											# verifica che la mossa sia valida
		return self.status[move//3][move%3]==" " 						# estrae la mossa 0-8 -> 0-2,0-2



	def makeMove(self, move): 											# applica la mossa alla griglia
		self.status[move//3][move%3]='x' if self.turn else 'o' 			# estrae la mossa 0-8 -> 0-2,0-2
		self.turn=not self.turn 										# cambia il turno



	def getWinner(self): 												# restituisce -1=da finire, 0=pareggio, 1=x, 2=o
		for row in self.status: 										# controlla le righe
			b, n=checkThree(row)
			if b:
				return n

		for col in range(3):											# controlla le colonne
			b, n=checkThree([self.status[row][col] for row in range(3)])# ottiene una colonna
			if b:
				return n

		b, n=checkThree([self.status[i][i] for i in range(3)])			# controlla la prima diagonale
		if b:
			return n

		b, n=checkThree([self.status[0][2], self.status[1][1], self.status[2][0]])# controlla la seconda diagonale
		if b:
			return n

		if any([' ' in row for row in self.status]): 					# controlla che ci siano caselle libere (verifica che ci siano spazi in ogni riga)
			return -1													# restituisce -1, da finire, la partita continua

		return 0														# nessuno degli altri casi, restuisce 0, pareggio



	def minimax(self, is_maximizing, alpha=-float('inf'), beta=float('inf')):
		"""
		Valuta la posizione corrente con l'algoritmo Minimax con potatura alfa-beta.
		is_maximizing: True se è il turno del bot (o), False se è il turno dell'avversario (x).
		alpha: miglior valore per il massimizzatore.
		beta:  miglior valore per il minimizzatore.
		Restituisce il punteggio dal punto di vista del bot:
			+1 se vince il bot,
			-1 se vince l'avversario,
			0 in caso di pareggio.
		"""
		winner = self.getWinner()
		if winner != -1:  												# stato terminale
			if winner == 2:   											# vince il bot (o)
				return 1
			elif winner == 1: 											# vince l'avversario (x)
				return -1
			else:             											# pareggio
				return 0

		if is_maximizing:
			best_score = -float('inf')
			for move in range(9):
				if self.isValid(move):
					new_grid = grid(deepcopy(self.status), self.turn)
					new_grid.makeMove(move)
					score = new_grid.minimax(False, alpha, beta)
					best_score = max(best_score, score)
					alpha = max(alpha, score)
					if alpha >= beta:   # potatura
						break
			return best_score
		else:
			best_score = float('inf')
			for move in range(9):
				if self.isValid(move):
					new_grid = grid(deepcopy(self.status), self.turn)
					new_grid.makeMove(move)
					score = new_grid.minimax(True, alpha, beta)
					best_score = min(best_score, score)
					beta = min(beta, score)
					if beta <= alpha:   # potatura
						break
			return best_score


	def bestMove(self):
		"""
		Calcola la migliore mossa per il bot (massimizzatore) usando minimax con alfa-beta.
		Restituisce l'indice della mossa (0-8).
		"""
		best_score = -float('inf')
		best_move = None
		for move in range(9):
			if self.isValid(move):
				new_grid = grid(deepcopy(self.status), self.turn)
				new_grid.makeMove(move)
				score = new_grid.minimax(False, -float('inf'), float('inf'))  # dopo la mossa del bot tocca all'avversario
				if score > best_score:
					best_score = score
					best_move = move
		return best_move



#ciclo principale
savefile=".interruptedgame.txt"											# nome del file di salvataggio, se inizia col punto è un file nascosto
prevgame=False															# crea prevgame, se viene caricata una partita interrotta il contenuto viene messo qui

if os.path.exists(savefile): 											# controlla se il file esiste (c'è una partita in sospeso)
	if 'n' not in input("C'è una partita a metà, continuare? [Y/n]: ").lower(): # chiede conferma all'utente
		with open(savefile, 'r') as f:									# apre il file in modalità lettura (r)
			prevgame=f.read()											# legge il file in prevgame

	os.remove(savefile)													# cancella il file

board=grid() if not prevgame else grid([list(prevgame[0:3]), list(prevgame[3:6]), list(prevgame[6:9])], prevgame[9]=='1') 	# se prevgame è una stringa (ha caricato una vecchia partita)
																															# crea la griglia a partire da quella
print("\n\n|0|1|2|\n|3|4|5|\n|6|7|8|", end="")
try:
	while board.getWinner()==-1:										# mentre la partita è in corso
		board.print()													# stampa la griglia
		move=int(input("Inserisci la tua mossa (0-8): ")) 				# ottiene la mossa dell'utente
		while not board.isValid(move):									# ripeti fino a quando la mossa non è valida
			move=int(input("Mossa non valida, riprova (0-8): ")) 		# ottiene la mossa dell'utente

		board.makeMove(move)											# applica la mossa dell'utente
		if board.getWinner()!=-1:										# se la partita è finita il bot non deve muovere
			break
		board.makeMove(board.bestMove())								# ottiene e applica la mossa del bot

	board.print()														# mostra la griglia
	match board.getWinner():											# controlla il risultato
		case 0:															# pareggio
			print("Pareggio.\n")
		case 1:															# vince x (il giocatore)
			print("Hai vinto!\n")
		case 2:															# vince o (il bot)
			print("Hai perso.\n")

except KeyboardInterrupt:												# se l'utente preme ctrl+c
	save='n' not in input("\nLa partita è ancora in corso, salvare? [Y/n]: ").lower() # ottiene il consenso dell'utente
	if save:
		with open(savefile, 'w') as f:									# apre il file in modalità scrittura (w)
			f.write(f"{''.join(chain.from_iterable(board.status))}{'1' if board.turn else '0'}")# salva lo stato